use clap::Parser;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::Write;
use std::iter::Iterator;
use std::path::Path;
use walkdir::WalkDir;
use zip::write::FileOptions;
use zip::ZipWriter;

/// 支持单个文件夹压缩
#[derive(Parser, Debug)]
#[command(author, version)]
pub struct Cli {
    /// 目录中需要跳过的目录或文件路径, 以压缩目录为起始点, 例如: 压缩目录为 /data/project, 需要过滤 /data/project/p/READEME.md, 输入 -i /p/READEME.md.
    /// 通过多次设置，可设置忽略多个目录或文件夹, 例如: -i /p/READEME.md -i /d/READEME.md 表示同时忽略 /p/READEME.md 和 /d/READEME.md
    #[arg(short, long)]
    ignore: Vec<String>,

    /// 放置压缩文件的目录, 请保证对目录有写入权限, 默认`当前操作目录`,
    #[arg(short, long, default_value=".", value_parser = target_dir_parser)]
    target_dir: String,

    /// 压缩文件名前缀，最终的得到的压缩文件名将是: {prefix_name}_{压缩文件md5值}.zip
    #[arg(short, long, default_value = "wzip")]
    prefix_name: String,

    /// 压缩的目录
    cdw: String,

    /// 其他压缩文件地址，它被先解压到需要被压缩目录, 再统一被压缩, 注意: 如果被压缩目录中，与其他压缩文件具有相同文件或目录，将被覆盖
    #[arg(short, long, value_name = "Zip_File_Path", value_parser = append_file_parser)]
    append_file: Option<String>,

    /// 支持压缩类型: stored/bzip2/zstd
    #[arg(short, long, default_value = "deflate", value_parser = compress_method_parser)]
    compress_method: zip::CompressionMethod,

    /// 是否输出忽略的文件或目录, 默认不输出
    #[arg(long)]
    print_ignore: bool,
}

fn main() {
    let cli = Cli::parse();

    if let Some(append_file) = &cli.append_file {
        println!("解压压缩文件中: {}", append_file);
        unzip_file(&append_file, &cli.cdw);
        println!("解压压缩文件成功");
    }

    println!("压缩目录中: {}", cli.cdw);
    let time_stamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_err(|d| format!("{:?}: 获取当前时间戳失败", d))
        .unwrap()
        .as_millis();
    let dist_path = Path::new(&cli.target_dir).join(format!("wzip_{}.zip", time_stamp));
    zip_dir(
        &cli.cdw,
        &dist_path,
        cli.compress_method,
        cli.ignore.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
        cli.print_ignore,
    );
    println!("压缩目录成功");

    println!("计算md5重命名中");
    let mut zip_file = File::open(&dist_path).unwrap();

    let mut md5_cxt = md5::Context::new();
    let mut buffer = [0u8; 4 * 1024];
    loop {
        let byte_read = zip_file.read(&mut buffer).unwrap();
        if byte_read == 0 {
            break;
        }
        md5_cxt.consume(&buffer[..byte_read]);
    }

    let md5_digest = md5_cxt.compute();
    let md5_str = format!("{md5_digest:x}");

    let new_dist_path =
        Path::new(&cli.target_dir).join(format!("{}_{}.zip", cli.prefix_name, md5_str));
    fs::rename(dist_path, new_dist_path).unwrap();
    println!("计算md5重命名成功");

    println!("压缩成功");
    println!("{:?}", cli);
}

fn zip_dir(
    src_dir: &str,
    dist_path: &Path,
    compress_method: zip::CompressionMethod,
    ignore: Vec<&str>,
    print_ignore: bool,
) {
    let file = File::create(dist_path).unwrap();
    let ignore = ignore.into_iter().map(|d| Path::new(d)).collect::<Vec<_>>();

    let walkdir = WalkDir::new(src_dir);
    let it = walkdir.into_iter();

    let mut zip = ZipWriter::new(file);
    let options = FileOptions::default()
        .compression_method(compress_method)
        .unix_permissions(0o755);

    let mut buffer = Vec::new();
    for entry in it {
        let entry = entry.unwrap();
        let path = entry.path();

        let name = path.strip_prefix(Path::new(src_dir)).unwrap();
        if let Some(ignore_path) = ignore.iter().find(|d| name.starts_with(d)) {
            if print_ignore {
                println!("由于规则 {:?} 忽略 {:?}", path, ignore_path);
            }
            continue;
        }

        // Write file or directory explicitly
        // Some unzip tools unzip files with directory paths correctly, some do not!
        if path.is_file() {
            // println!("adding file {path:?} as {name:?} ...");
            #[allow(deprecated)]
            zip.start_file_from_path(name, options).unwrap();
            let mut f = File::open(path).unwrap();

            f.read_to_end(&mut buffer).unwrap();
            zip.write_all(&buffer).unwrap();
            buffer.clear();
        } else if !name.as_os_str().is_empty() {
            // Only if not root! Avoids path spec / warning
            // and mapname conversion failed error on unzip
            // println!("adding dir {path:?} as {name:?} ...");
            #[allow(deprecated)]
            zip.add_directory_from_path(name, options).unwrap();
        }
    }
    zip.finish().unwrap();
}

fn unzip_file(file_path: &str, target_dir: &str) {
    let fname = Path::new(file_path);
    let file = File::open(fname).unwrap();

    let mut archive = zip::ZipArchive::new(file).unwrap();
    let target_dir = Path::new(target_dir);

    for i in 0..archive.len() {
        let mut file = archive.by_index(i).unwrap();
        let mut outpath = match file.enclosed_name() {
            Some(path) => path.to_owned(),
            None => continue,
        };
        outpath = target_dir.join(outpath);

        if (*file.name()).ends_with('/') {
            fs::create_dir_all(&outpath).unwrap();
        } else {
            if let Some(p) = outpath.parent() {
                if !p.exists() {
                    fs::create_dir_all(p).unwrap();
                }
            }
            let mut outfile = fs::File::create(&outpath)
                .map_err(|d| format!("{}\n{:?} 在目录中已存在且无权限覆盖?", d, outpath))
                .unwrap();
            std::io::copy(&mut file, &mut outfile).unwrap();
        }

        // Get and Set permissions
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;

            if let Some(mode) = file.unix_mode() {
                fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap();
            }
        }
    }
}

fn append_file_parser(append: &str) -> Result<String, String> {
    let meta = fs::metadata(append).map_err(|err| err.to_string())?;
    if !meta.is_file() || !append.ends_with(".zip") {
        return Err("只支持 zip[.zip] 文件".to_owned());
    }
    if meta.permissions().readonly() {
        return Err("对目录无写权限".to_owned());
    }
    Ok(append.to_owned())
}

fn target_dir_parser(target: &str) -> Result<String, String> {
    let meta = fs::metadata(target).map_err(|err| err.to_string())?;
    if !meta.is_dir() || meta.permissions().readonly() {
        return Err("对目录无写权限".to_owned());
    }
    Ok(target.to_owned())
}

fn compress_method_parser(target: &str) -> Result<zip::CompressionMethod, String> {
    match target {
        "deflate" => Ok(zip::CompressionMethod::Deflated),
        "store" => Ok(zip::CompressionMethod::Stored),
        "bzip2" => Ok(zip::CompressionMethod::Bzip2),
        "zstd" => Ok(zip::CompressionMethod::Zstd),
        _ => Err(format!("不支持 {} 压缩类型", target)),
    }
}
